import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;


public class BinaryFileIO {
	private String fileName;
	private String rsFileName;
	private int headerLength;
	private int rowLength;

	/**
	 * 
	 * @param fileName binary file name
	 * @param rsFileName row state file name of binary file (for deleted or available row)
	 * @param headerLength header length, can be 0 if no header need
	 * @param rowLength the length of one row in a table
	 */
	public BinaryFileIO(String fileName, String rsFileName, int headerLength, int rowLength) {
		this.fileName = fileName;
		this.rsFileName = rsFileName;
		createFile(fileName);
		createFile(rsFileName);
		this.headerLength = headerLength;
		this.rowLength = rowLength;
	}
	
	// create a binary file if not exists
	private void createFile(String fileName) {
		File file = new File("."+ File.separator + fileName);
		if (!file.exists()) {
			System.out.println("Creating binary file: " + fileName);
			file.getParentFile().mkdirs();
			try {
				file.createNewFile();
			} catch (IOException e) {
				System.out.println("File not created: " + fileName);
			}
		}
	}
	
	// change the header's value (the header contains only an offset now)
	public void updateHeader(byte[] header) {
		FileOutputStream fileOutputStream = null;
		FileChannel fileChannel = null;
		try {
			try {
				fileOutputStream = new FileOutputStream(fileName, true);
				fileChannel = fileOutputStream.getChannel();
				fileChannel.write(ByteBuffer.wrap(header), 0);
			} finally {
				fileChannel.close();
				fileOutputStream.close();
			}
		} catch (FileNotFoundException ex) {
			System.out.println("File not found.");
		} catch (IOException ex) {
			System.out.println(ex);
		}
	}

	// insert a row to the binary file depending on the row state file
	public int insertRow(byte[] row) {
		int rowNumber = 0; // "row number" of deleted record (first row is 0) (used only if isDeleted == true)
		boolean isDeleted = false; // row is deleted or not
		
		// check if there is a deleted row in the binary file
		FileInputStream rowStateInputStream = null;
		try{
			try{
				rowStateInputStream = new FileInputStream(rsFileName);
				int rowState;
				while ((rowState = rowStateInputStream.read()) != -1) {
					if (rowState == 1) { // deleted row
						isDeleted = true;
						break;
					}
					rowNumber++;
				}
			} finally {
				rowStateInputStream.close();
			}
		} catch (FileNotFoundException ex) {
			System.out.println("File not found.");
		} catch (IOException ex) {
			System.out.println(ex);
		}
		
		int offset = -1;
		
		// insert new row to the file and update the row state file too
		FileOutputStream fileOutputStream = null;
		FileChannel fileChannel = null;
		FileOutputStream rowStateOutputStream = null;
		FileChannel rowStateChannel = null;
		try{
			try{
				fileOutputStream = new FileOutputStream(fileName, true);
				rowStateOutputStream = new FileOutputStream(rsFileName, true);
				
				fileChannel = fileOutputStream.getChannel();
				rowStateChannel = rowStateOutputStream.getChannel();
				if (isDeleted) {
					
					offset = headerLength + rowNumber * rowLength;
					fileChannel.write(ByteBuffer.wrap(row), offset); // "update" a deleted row
					rowStateChannel.write(ByteBuffer.wrap(new byte[] {0}), rowNumber); // set row state to not deleted
				} else {
					offset = (int)fileChannel.size();
					fileChannel.position(offset);
					fileChannel.write(ByteBuffer.wrap(row)); // insert a row to the end of file
					rowStateChannel.position(rowStateChannel.size());
					rowStateChannel.write(ByteBuffer.wrap(new byte[] {0})); // insert a row state to the end of "row state file"
				}		
			} finally {
				fileChannel.close();
				fileOutputStream.close();
				rowStateChannel.close();
				rowStateOutputStream.close();
			}
		} catch (FileNotFoundException ex) {
			System.out.println("File not found.");
		} catch (IOException ex) {
			System.out.println(ex);
		}
		return offset;
	}

	// change the correct byte in the row state file -> it means that row is deleted
	public void deleteRow(int offset) {
		FileOutputStream rowStateOutputStream = null;
		FileChannel rowStateChannel = null;
		try {
			try {
				rowStateOutputStream = new FileOutputStream(rsFileName, true);
				rowStateChannel = rowStateOutputStream.getChannel();
				int rowNumber = (offset - headerLength) / rowLength;
				rowStateChannel.write(ByteBuffer.wrap(new byte[] {1}), rowNumber);
			} finally {
				rowStateChannel.close();
				rowStateOutputStream.close();
			}
		} catch (FileNotFoundException ex) {
			System.out.println("File not found.");
		} catch (IOException ex) {
			System.out.println(ex);
		}
	}
	
	// update a row in the binary file
	public void updateRow(byte[] row, int offset) {
		FileOutputStream fileOutputStream = null;
		FileChannel fileChannel = null;
		try{
			try{
				fileOutputStream = new FileOutputStream(fileName, true);
				fileChannel = fileOutputStream.getChannel();
				fileChannel.write(ByteBuffer.wrap(row), offset); // "update" a row	
			} finally {
				fileChannel.close();
				fileOutputStream.close();
			}
		} catch (FileNotFoundException ex) {
			System.out.println("File not found.");
		} catch (IOException ex) {
			System.out.println(ex);
		}
	}
	
	/**
	 * 
	 * @param offset an offset where a row is started in the file
	 * @param rowLength the "row length" in bytes that we want to read
	 * @return a row started from offset (the row length is rowLength) or null if something is incorrect  
	 */
	public byte[] readRow(int offset, int rowLength) {	
		FileInputStream fileInputStream = null;
		try {
			try {
				fileInputStream = new FileInputStream(fileName);
				
				FileChannel ch = fileInputStream.getChannel();
				
				ByteBuffer byteBuffer = ByteBuffer.allocateDirect(rowLength);
				
				if (ch.read(byteBuffer, offset) == -1)
					return null;

				byteBuffer.position(0);
				byte[] row = new byte[rowLength];
				byteBuffer.get(row);
				
				return row;
				
			} finally {
				fileInputStream.close();
			}
		} catch (FileNotFoundException ex) {
			System.out.println("File not found.");
		} catch (IOException ex) {
			System.out.println(ex);
		}
		return null;
	}
	
	// convert byte[] to Node
	public Node toNode(byte[] row, int nodeOffset){
		Node node = new Node();
//		io.insertRow(new byte[]{1,  3,  0,0,0,3, 0,0,0,21,  0,0,0,2, 0,0,0,45,  0,0,0,5, 0,0,0,71,  0,0,0,0});
		node.setmIsLeafNode(row[0] != 0);
		node.setmNumKeys(row[1]);
		node.setOffset(nodeOffset);
		if (node.ismIsLeafNode()){
			for (int i = 0; i < row[1]; i++){
				 node.setmObject(ByteBuffer.wrap(row, i*DomainObjectChecker.DEFAULT_INT_LENGTH*2+2, DomainObjectChecker.DEFAULT_INT_LENGTH).getInt(), i);
			}
			for (int i = 0; i < row[1]; i++){
				 node.setmKey(ByteBuffer.wrap(row, i*DomainObjectChecker.DEFAULT_INT_LENGTH*2+2+DomainObjectChecker.DEFAULT_INT_LENGTH, DomainObjectChecker.DEFAULT_INT_LENGTH).getInt(), i);
			}
			node.setmNextNode(ByteBuffer.wrap(row, row.length-DomainObjectChecker.DEFAULT_INT_LENGTH, DomainObjectChecker.DEFAULT_INT_LENGTH).getInt());
		} else {
			for (int i = 0; i <= row[1]; i++){
				 node.setmChildNode(ByteBuffer.wrap(row, i*DomainObjectChecker.DEFAULT_INT_LENGTH*2+2, DomainObjectChecker.DEFAULT_INT_LENGTH).getInt(), i);
			}
			for (int i = 0; i < row[1]; i++){
				 node.setmKey(ByteBuffer.wrap(row, i*DomainObjectChecker.DEFAULT_INT_LENGTH*2+2+DomainObjectChecker.DEFAULT_INT_LENGTH, DomainObjectChecker.DEFAULT_INT_LENGTH).getInt(), i);
			}
		}
		return node;
	}
	
	public byte[] toByteArray(Node node, int rowLength) {
		byte[] byteArray = new byte[rowLength];
		byteArray[0] = (byte) (node.ismIsLeafNode() ? 1 : 0);
		byteArray[1] = (byte) node.getmNumKeys();
		if (node.ismIsLeafNode()) {
			for (int i = 0; i < node.getmNumKeys(); i++){
				byte[] object = ByteBuffer.allocate(DomainObjectChecker.DEFAULT_INT_LENGTH).putInt((int)node.getmObject(i)).array();
				byteArray[i * 8 + 2 + 0] = object[0];
				byteArray[i * 8 + 2 + 1] = object[1];
				byteArray[i * 8 + 2 + 2] = object[2];
				byteArray[i * 8 + 2 + 3] = object[3];
				byte[] key = ByteBuffer.allocate(DomainObjectChecker.DEFAULT_INT_LENGTH).putInt((int)node.getmNumKey(i)).array();
				byteArray[i * 8 + 2 + 4 + 0] = key[0];
				byteArray[i * 8 + 2 + 4 + 1] = key[1];
				byteArray[i * 8 + 2 + 4 + 2] = key[2];
				byteArray[i * 8 + 2 + 4 + 3] = key[3];
			}
			byte[] nextNodeOffset = ByteBuffer.allocate(DomainObjectChecker.DEFAULT_INT_LENGTH).putInt((int)node.getmNextNodeOffset()).array();
			byteArray[rowLength - 4 + 0] = nextNodeOffset[0];
			byteArray[rowLength - 4 + 1] = nextNodeOffset[1];
			byteArray[rowLength - 4 + 2] = nextNodeOffset[2];
			byteArray[rowLength - 4 + 3] = nextNodeOffset[3];
		} else {
			for (int i = 0; i <= node.getmNumKeys(); i++){
				byte[] object = ByteBuffer.allocate(DomainObjectChecker.DEFAULT_INT_LENGTH).putInt((int)node.getmChildNodeOffset(i)).array();
				byteArray[i * 8 + 2 + 0] = object[0];
				byteArray[i * 8 + 2 + 1] = object[1];
				byteArray[i * 8 + 2 + 2] = object[2];
				byteArray[i * 8 + 2 + 3] = object[3];
				if (i != node.getmNumKeys()) {
					byte[] key = ByteBuffer.allocate(DomainObjectChecker.DEFAULT_INT_LENGTH).putInt((int)node.getmNumKey(i)).array();
					byteArray[i * 8 + 2 + 4 + 0] = key[0];
					byteArray[i * 8 + 2 + 4 + 1] = key[1];
					byteArray[i * 8 + 2 + 4 + 2] = key[2];
					byteArray[i * 8 + 2 + 4 + 3] = key[3];
				}
			}
		}
		return byteArray;
	}
}